Skip to content

Conversation

@lia-viam
Copy link
Collaborator

Introduces a one-and-only Instance class for the SDK. For the time being this only has a Registry, but it will pave the way for inclusion of better logging and resource-level logging in particular.

Some of the machinery in instance.cpp for singleton management was adapted from https://github.com/mongodb/mongo-cxx-driver/blob/02ad73bc306eb9044eb691de96ad41c13107738d/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/instance.cpp

A few classes were updated to now get a pointer to a Registry rather than calling the no-longer-static Registry methods. The existence of Instance::current() means technically these could just get the Registry from the current instance, or possibly that Registry could have a static Registry& current() method...open to feedback here.

Every example has been updated to define an instance object, with the same boilerplate comment put in front explaining its usage

@lia-viam lia-viam requested review from acmorrow and stuqdog February 25, 2025 20:49
@lia-viam lia-viam requested a review from a team as a code owner February 25, 2025 20:49
@lia-viam lia-viam requested review from purplenicole730 and removed request for a team February 25, 2025 20:49
std::shared_ptr<Resource> ModuleService::get_parent_resource_(const Name& name) {
if (!parent_) {
parent_ = RobotClient::at_local_socket(parent_addr_, {0, boost::none});
parent_ = RobotClient::at_local_socket(parent_addr_, {0, boost::none}, registry_);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question for reviewers--I am wondering if RobotClient should be part of the Instance as well, it seems like we always want it? Looking through the examples I believe the only scenario where RobotClient is not constructed explicitly is one where we just construct a ModuleService but that ends up constructing a RobotClient in this method anyway.

Looking ahead to the logger PR, the logger will need a RobotClient for the gRPC Log call, so we may want to avoid a situation where RobotClient is getting delayed instantiation and just construct one on application startup.

Copy link
Member

@stuqdog stuqdog Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd prefer to keep RobotClient distinct.

  1. I think of the Instance as being a bit of a black box; the user knows they need one and that they need to keep it around, but they don't need to know anything about what's inside. By contrast, a RobotClient is something that users may interact with directly and frequently.
  2. There should only ever be one live instance, whereas one could imagine a user writing client code that interacts with multiple robots and so therefore has multiple robot clients. We could have some sort of map from name to RobotClient inside the instance but then a user starts needing to know about Instance APIs and we're adding new layers of abstraction between the user and their access to robot APIs and I think it just starts to get a bit complicated.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those points make sense. Follow-ups

  • if there are multiple RobotClient should log messages be getting sent to the Log call of each of them?
  • given the comment about Instance insulation, how do we feel about now having to do inst.registry() for some of the constructors?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm good questions!

if there are multiple RobotClient should log messages be getting sent to the Log call of each of them?

I think this is a question we can largely ignore, thankfully. If there are multiple RobotClients then the user is interacting with purely client code and so the logs should be showing up in the shell, so we dont have to call a RobotClient::log at all.

If on the other hand, we do have to send logs via the Log method then we're dealing with a module. But, each module is started up as its own process for a single Robot, so we can always only send logs via the Log call to the module's parent.

given the comment about Instance insulation, how do we feel about now having to do inst.registry() for some of the constructors?

Caveat: I will readily confess to not being particularly hip to C++ conventions. If what I'm saying flies in the face of idiomatic C++ design then obviously we should prefer remaining idiomatic.

I personally prefer to not do that. Per our offline conversation yesterday and to reiterate some of that: I'd love to hide as much as possible from users about the instance, and ideally not really expose any methods at all to the user about it. If it's needed in particular cases, I'd prefer to just pass the instance as an argument and let the RobotClient constructor, e.g., deal with getting the Registry from a private method via some friend class setup.

I don't think this is always clean and easy to do, as when registering custom resources, but I imagine we could add a register_resource method to a RobotClient that under the hood interacts with the registry. I think this is a bit cleaner and a bit more intuitive (when you want a robot client to deal with a custom resource, you register that resource with the client!). A possible downside here is this pattern is slightly misleading: we're saying "register it with the robot client" but in fact we're registering with the singleton registry, which means another robot client would also be able to work with that resource type. But I don't think this is particularly harmful, and we can work around it if it is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that's helpful--in that case I can have a log backend which binds to a RobotClient but only register it with the logging core when that RobotClient is the parent of a ModuleService !

As for the other stuff I've made some further updates which I think clean up the usage a lot, using the current method as discussed yesterday

Copy link
Member

@stuqdog stuqdog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this is super exciting! Thanks so much for putting this together, it's a complicated one but a big win.

I've done a first pass; there's a lot to think about here so I think there will be more back and forth but here are some initial thoughts.

status get_machine_status() const;

private:
Registry* registry_;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(minor) I don't feel too strongly, but is this change necessary? It seems to me that we could just use Registry::get() in the refresh() call rather than holding onto this pointer. It might be marginally less performant to have to get the Registry every time we refresh, but I don't expect it's a significant cost and it helps simplify the info that a user is exposed to in the headers.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe cc @acmorrow when he gets a sec, but wondering if Registry can just get friend access to instance and be able to return a reference to the registry member rather than having to access it through Instance::current with the atomic::loads. My though is that since we say it's UB to use any Viam types without an Instance already constructed we can skip the whole "create if not exists" logic of Instance::current

I agree that the performance penalty can probably be ignored here, but if so then we could do

static Registry& Registry::get()
{
    static Registry& result = Instance::current().impl_->registry;
    return result;
}

in which case the static would only be initialized one-and-done style


// Construct the module service and tell it where to place the socket path.
auto module_service = std::make_shared<vsdk::ModuleService>(socket_path);
auto module_service = std::make_shared<vsdk::ModuleService>(socket_path, inst.registry());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this change is out of date? It doesn't look like the ModuleService takes a registry as part of its constructor.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops yes good catch, tflite is not built in CI and hadn't rebuilt it locally

/// @brief Instance management for Viam C++ SDK applications.
/// This is a single instance class which is responsible for global setup and teardown related to
/// the SDK. An Instance must be constructed before doing anything else in a program, and it must
/// remain alive in a valid state for the duration of the program. Creating multiple overlapping
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How difficult would it be to get rid of the overlapping clause and just make it an error to create multiple instances, period?

When we create an instance we create a new registry and run a second registry::initialize(), which sets up gRPC reflection a second time. I could easily imagine this getting pretty complicated and having unforeseen side effects. It's definitely an edge case, but I think just being upfront that users can only ever create one instance helps simplify things.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no difficulty per se, we can just tell people "don't ever do this, it's UB" 🙂


const std::shared_ptr<const ModelRegistration> reg = Registry::lookup_model(cfg.name());
const std::shared_ptr<const ModelRegistration> reg =
parent.registry_->lookup_model(cfg.name());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above re: parent.registry_


const std::shared_ptr<const ModelRegistration> reg =
Registry::lookup_model(cfg.api(), cfg.model());
parent.registry_->lookup_model(cfg.api(), cfg.model());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above re: parent.registry_

std::string const& resource_name);
std::shared_ptr<Resource> get_parent_resource_(const Name& name);

Registry* registry_;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above comments on this being a class data member.

static std::unordered_map<API, std::shared_ptr<const ResourceClientRegistration>> client_apis_;
static std::unordered_map<API, std::shared_ptr<const ResourceServerRegistration>> server_apis_;
mutable std::mutex lock_;
bool initialized_{false};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(minor) I think we can get rid of this entirely now. Now that we have a single instance of a registry that gets automatically initialized when the Instance object is created, we don't need to track this anymore.

Comment on lines +45 to +47
if (current == reinterpret_cast<Instance*>(&sentinel)) {
throw Exception("instance was destroyed");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(q) pardon my ignorance, I don't think I fully understand this. Under what conditions will this exception throw?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait actually this gets to your comment above about making it an error to have multiple instances even if non-overlapping, so it already is an error to have multiple of them

Instance();
~Instance();

static Instance& current();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to doc-comment this, particularly if it is mostly intended for testing.

};

Registry& Registry::get() {
static Registry& result = Instance::current().impl_->registry;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • This will bring Instance into existence even if one wasn't created, I think. Those maybe aren't the right semantics, since then a user of the library could get away with not creating an Instance themselves.
  • Why the static& rather than just calling Instance::current() each time.

Copy link
Collaborator Author

@lia-viam lia-viam Mar 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I think you're right that those aren't the semantics we want--maybe it should be something like Instance::current(bool create_if_not_exists = false)
  • This was the result of a discussion me and @stuqdog were going back and forth on, and we thought that Instance should be a total black box so that it's in some sense an implementation detail that Instance has-a Registry or, later, Logging-Core. In either case though, I think that the divergences from our use cases and Mongo's mean that Instance::current does not end up being unit tests only. But from the "black box" perspective, the user doesn't know that Instance has a Registry, they just know 1) I have to create an Instance before doing anything, and 2) I can only access an instance of Registry through its static get so I can't create them as I please

grpc::reflection::InitProtoReflectionServerBuilderPlugin();
}

std::unordered_map<std::string, std::shared_ptr<const ModelRegistration>> Registry::resources_;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

class Registry {
public:
/// @brief Get the application-wide instance of Registry.
static Registry& get();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or instance? I also wonder a bit why to expose this on Registry rather than on Instance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair question--elaborated in another comment

@lia-viam lia-viam requested review from acmorrow and stuqdog March 12, 2025 18:08
Copy link
Member

@stuqdog stuqdog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, a couple nits but otherwise lgtm!

~Instance();

/// @brief Get the current Instance according to the Creation behavior.
/// Calling current(Creation::open_existing) when an instance has not yet been constructed is an
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(minor) I'd put the error case in a @throws line for clearer doxygen docs.

#include <viam/sdk/common/private/instance.hpp>
#include <viam/sdk/registry/registry.hpp>

#include <atomic>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) can we put this include block above the viam/sdk include block?

@lia-viam lia-viam merged commit 9dc2143 into viamrobotics:main Mar 13, 2025
4 checks passed
@lia-viam lia-viam deleted the registry-member branch March 13, 2025 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants